/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Attributes } from './Attributes';
import { Document } from './Document';
import { Node } from './Node';
import List from '@ohos.util.List';
import { Elements } from '../select/Elements';
import { Validate } from '../helper/Validate';
import { Tag } from '../parser/Tag';
import { NodeUtils } from './NodeUtils';
import { TextNode } from './TextNode';
import { DataNode } from './DataNode';
import { Selector } from '../select/Selector';
import { Evaluator } from '../select/Evaluator';
import { Collector } from '../select/Collector';
import { QueryParser } from '../select/QueryParser';
import { TokenQueue } from '../parser/TokenQueue';
import { StringJoiner, StringUtil } from '../internal/StringUtil';
import { Normalizer } from '../internal/Normalizer';
import { NodeTraversor } from '../select/NodeTraversor';
import { CDataNode } from './CDataNode';
import { NodeFilter } from '../select/NodeFilter';
import { NodeVisitor } from '../select/NodeVisitor';
// import { Comment } from './Comment';
import { Character } from '../util/Character';
import { Range } from './Range';
import constants from '../util/Constants';
import { Syntax } from './Syntax';
import { OutputSettings } from './OutputSettings';
import { EvaluatorId } from '../select/evaluator/EvaluatorId';
import { EvaluatorClass } from '../select/evaluator/EvaluatorClass';
import { EvaluatorAttributeStarting } from '../select/evaluator/EvaluatorAttributeStarting';
import { EvaluatorAttributeWithValue } from '../select/evaluator/EvaluatorAttributeWithValue';
import { EvaluatorAttributeWithValueNot } from '../select/evaluator/EvaluatorAttributeWithValueNot';
import { EvaluatorAttributeWithValueStarting } from '../select/evaluator/EvaluatorAttributeWithValueStarting';
import { EvaluatorAttributeWithValueEnding } from '../select/evaluator/EvaluatorAttributeWithValueEnding';
import { EvaluatorAttributeWithValueContaining } from '../select/evaluator/EvaluatorAttributeWithValueContaining';
import { EvaluatorAttributeWithValueMatching } from '../select/evaluator/EvaluatorAttributeWithValueMatching';
import { EvaluatorIndexLessThan } from '../select/evaluator/EvaluatorIndexLessThan';
import { EvaluatorIndexGreaterThan } from '../select/evaluator/EvaluatorIndexGreaterThan';
import { EvaluatorIndexEquals } from '../select/evaluator/EvaluatorIndexEquals';
import { EvaluatorContainsText } from '../select/evaluator/EvaluatorContainsText';
import { EvaluatorContainsOwnText } from '../select/evaluator/EvaluatorContainsOwnText';
import { EvaluatorMatches } from '../select/evaluator/EvaluatorMatches';
import { EvaluatorMatchesOwn } from '../select/evaluator/EvaluatorMatchesOwn';
import { EvaluatorAllElements } from '../select/evaluator/EvaluatorAllElements';
import { EvaluatorTag } from '../select/Evaluator/EvaluatorTag';
import { EvaluatorAttribute } from '../select/evaluator/EvaluatorAttribute';
import { Comment } from './Comment';
import { FilterResult } from '../select/FilterResult';
/**
 An HTML Element consists of a tag name, attributes, and child nodes (including text nodes and other elements).
 <p>
 From an Element, you can extract data, traverse the node graph, and manipulate the HTML.
 */
export class Element extends Node {
  private static readonly _EmptyChildren: List<Element> = new List()
  private static readonly _ClassSplit = '\\s+'
  private static readonly _BaseUriKey = constants.internalKey("baseUri")
  private _tag: Tag
  // points to child elements shadowed from node children
  private _shadowChildrenRef: List<Element>
  _childNodes: List<Node>
  // field is nullable but all methods for attributes are non-null
  _attributes: Attributes

  /**
   * Create a new, standalone Element. (Standalone in that it has no parent.)
   *
   * @param tag tag of this element
   * @param baseUri the base URI (optional, may be null to inherit from parent, or "" to clear parent's)
   * @param attributes initial attributes (optional, may be null)
   * @see #appendChild(Node)
   * @see #appendElement(String)
   */
  constructor(tag: Tag, baseUri: string = "", attributes: Attributes = null) {
    super();
    Validate.notNull(tag)
    this._childNodes = Element.EmptyNodes
    this._attributes = attributes
    this._tag = tag
    if (baseUri !== undefined && baseUri !== null) {
      this.setBaseUri(baseUri)
    }
  }

  /**
   Internal test to check if a nodelist object has been created.
   */
  hasChildNodes(): boolean {
    return this._childNodes !== Element.EmptyNodes
  }

  protected ensureChildNodes(): List<Node> {
    if (this._childNodes === undefined || this._childNodes === null || this._childNodes === Element.EmptyNodes) {
      this._childNodes = new List<Node>()
    }
    return this._childNodes
  }

  protected hasAttributes(): boolean {
    return this._attributes !== undefined && this._attributes !== null
  }

  public attributes(): Attributes {
    if (this._attributes === undefined || this._attributes === null) {
      this._attributes = new Attributes()
    }
    return this._attributes
  }

  public baseUri(): string {
    return Element.searchUpForAttribute(this, Element._BaseUriKey)
  }

  private static searchUpForAttribute(start: Element, key: string): string {
    let el = start
    while (el !== undefined && el !== null) {
      if (el._attributes !== undefined && el._attributes !== null && el._attributes.hasKey(key)) {
        return el._attributes.get(key)
      }
      el = el.parent()
    }
    return ""
  }

  public doSetBaseUri(baseUri: string) {
    this.attributes().put(Element._BaseUriKey, baseUri)
  }

  public childNodeSize(): number {
    return this._childNodes.length
  }

  public nodeName(): string {
    return this._tag.getName()
  }

  /**
   * Get the name of the tag for this element. E.g. {@code div}. If you are using {@link ParseSettings#preserveCase
   * case preserving parsing}, this will return the source's original case.
   *
   * @return the tag name
   */
  public getTagName(): string {
    return this._tag.getName()
  }
  /**
   * Get the normalized name of this Element's tag. This will always be the lower-cased version of the tag, regardless
   * of the tag case preserving setting of the parser. For e.g., {@code <DIV>} and {@code <div>} both have a
   * normal name of {@code div}.
   * @return normal name
   */
  public normalName(): string {
    return this._tag.normalName()
  }
  /**
   * Change (rename) the tag of this element. For example, convert a {@code <span>} to a {@code <div>} with
   * {@code el.tagName("div");}.
   *
   * @param tagName new tag name for this element
   * @return this element, for chaining
   * @see Elements#tagName(String)
   */
  public setTagName(tagName: string): Element {
    Validate.notEmptyParam(tagName, "tagName")
    this._tag = Tag.valueOf(tagName, NodeUtils.parser(this).getSettings())
    return this
  }

  /**
   * Get the Tag for this element.
   *
   * @return the tag object
   */
  public getTag(): Tag {
    return this._tag
  }

  /**
   * Test if this element is a block-level element. (E.g. {@code <div> == true} or an inline element
   * {@code <span> == false}).
   *
   * @return true if block, false if not (and thus inline)
   */
  public isBlock(): boolean {
    return this._tag.isBlock()
  }

  /**
   * Get the {@code id} attribute of this element.
   *
   * @return The id attribute, if present, or an empty string if not.
   */
  public getId(): string {
    return (this._attributes !== undefined && this._attributes !== null) ? this._attributes.getIgnoreCase('id') : ''
  }

  /**
   Set the {@code id} attribute of this element.
   @param id the ID value to use
   @return this Element, for chaining
   */
  public setId(id: string) {
    Validate.notNull(id)
    this.setAttr('id', id)
    return this
  }

  /**
   * Set an attribute value on this element. If this element already has an attribute with the
   * key, its value is updated; otherwise, a new attribute is added.
   *
   * @return this element
   */
  public setAttr(attributeKey: string, attributeValue: string): Element {
    super.setAttr(attributeKey, attributeValue)
    return this
  }

  /**
   * Set a boolean attribute value on this element. Setting to <code>true</code> sets the attribute value to "" and
   * marks the attribute as boolean so no value is written out. Setting to <code>false</code> removes the attribute
   * with the same key if it exists.
   *
   * @param attributeKey the attribute key
   * @param attributeValue the attribute value
   *
   * @return this element
   */
  public attrBoolean(attributeKey: string, attributeValue: boolean): Element {
    this.attributes().putBoolean(attributeKey, attributeValue)
    return this
  }

  /**
   * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key
   * starting with "data-" is included the dataset.
   * <p>
   * E.g., the element {@code <div data-package="jsoup" data-language="Java" class="group">...} has the dataset
   * {@code package=jsoup, language=java}.
   * <p>
   * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected
   * in the other map.
   * <p>
   * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector.
   * @return a map of {@code key=value} custom data attributes.
   */
  public getDataset() {
    return this.attributes().dataset()
  }

  public parent(): Element {
    return this.parentNode as Element
  }

  /**
   * Get this element's parent and ancestors, up to the document root.
   * @return this element's stack of parents, starting with the closest first.
   */
  parents(): Elements {
    let parents = new Elements()
    let parent = this.parent()
    while (parent !== undefined && parent !== null && !parent.isNode('#root')) {
      parents.add(parent)
      parent = parent.parent()
    }
    return parents
  }

  /**
   * Get a child element of this element, by its 0-based index number.
   * <p>
   * Note that an element can have both mixed Nodes and Elements as children. This method inspects
   * a filtered list of children that are elements, and the index is based on that filtered list.
   * </p>
   *
   * @param index the index number of the element to retrieve
   * @return the child element, if it exists, otherwise throws an {@code IndexOutOfBoundsException}
   * @see #childNode(int)
   */
  child(index: number): Element {
    return this.childElementsList().get(index)
  }

  /**
   * Get the number of child nodes of this element that are elements.
   * <p>
   * This method works on the same filtered list like {@link #child(int)}. Use {@link #childNodes()} and {@link
   * #childNodeSize()} to get the unfiltered Nodes (e.g. includes TextNodes etc.)
   * </p>
   *
   * @return the number of child nodes that are elements
   * @see #children()
   * @see #child(int)
   */
  childrenSize(): number {
    return this.childElementsList().length
  }

  /**
   * Get this element's child elements.
   * <p>
   * This is effectively a filter on {@link #childNodes()} to get Element nodes.
   * </p>
   * @return child elements. If this element has no children, returns an empty list.
   * @see #childNodes()
   */
  children(): Elements {
    return new Elements(this.childElementsList())
  }

  /**
   * Maintains a shadow copy of this element's child elements. If the nodelist is changed, this cache is invalidated.
   * TODO - think about pulling this out as a helper as there are other shadow lists (like in Attributes) kept around.
   * @return a list of child elements
   */
  childElementsList(): List<Element> {
    if (this.childNodeSize() === 0) {
      // short circuit creating empty
      return Element._EmptyChildren
    }
    if (this._shadowChildrenRef === undefined || this._shadowChildrenRef === null) {
      let size = this.childNodes().length
      this._shadowChildrenRef = new List<Element>()
      for (let index = 0; index < size; index++) {
        let node = this.childNodes().get(index)
        if (node instanceof Element) {
          this._shadowChildrenRef.add(node as Element)
        }
      }
    }
    return this._shadowChildrenRef
  }

  /**
   * Clears the cached shadow child elements.
   */
  nodelistChanged() {
    super.nodelistChanged()
    this._shadowChildrenRef = null
  }

  /**
   * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated.
   * <p>
   * This is effectively a filter on {@link #childNodes()} to get Text nodes.
   * @return child text nodes. If this element has no text nodes, returns an
   * empty list.
   * </p>
   * For example, with the input HTML: {@code <p>One <span>Two</span> Three <br> Four</p>} with the {@code p} element selected:
   * <ul>
   *     <li>{@code p.text()} = {@code "One Two Three Four"}</li>
   *     <li>{@code p.ownText()} = {@code "One Three Four"}</li>
   *     <li>{@code p.children()} = {@code Elements[<span>, <br>]}</li>
   *     <li>{@code p.childNodes()} = {@code List<Node>["One ", <span>, " Three ", <br>, " Four"]}</li>
   *     <li>{@code p.textNodes()} = {@code List<TextNode>["One ", " Three ", " Four"]}</li>
   * </ul>
   */
  public textNodes(): List<TextNode> {
    let textNodes = new List<TextNode>()
    for (let node of this.childNodes()) {
      if (node instanceof TextNode) {
        textNodes.add(node as TextNode)
      }
    }
    return textNodes
  }

  /**
   * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated.
   * <p>
   * This is effectively a filter on {@link #childNodes()} to get Data nodes.
   * </p>
   * @return child data nodes. If this element has no data nodes, returns an
   * empty list.
   * @see #data()
   */
  dataNodes(): List<DataNode> {
    let dataNodes = new List<DataNode>()
    for (let node of this.childNodes()) {
      if (node instanceof DataNode) {
        dataNodes.add(node as DataNode)
      }
    }
    return dataNodes
  }

  /**
   * Find elements that match the {@link Selector} CSS query, with this element as the starting context. Matched elements
   * may include this element, or any of its children.
   * <p>This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because
   * multiple filters can be combined, e.g.:</p>
   * <ul>
   * <li>{@code el.select("a[href]")} - finds links ({@code a} tags with {@code href} attributes)
   * <li>{@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely)
   * </ul>
   * <p>See the query syntax documentation in {@link org.jsoup.select.Selector}.</p>
   * <p>Also known as {@code querySelectorAll()} in the Web DOM.</p>
   *
   * @param cssQuery a {@link Selector} CSS-like query
   * @return an {@link Elements} list containing elements that match the query (empty if none match)
   * @see Selector selector query syntax
   * @see QueryParser#parse(String)
   * @throws Selector.SelectorParseException (unchecked) on an invalid CSS query.
   */
  select(cssQuery: string | Evaluator): Elements {
    return Selector.select(cssQuery, this)
  }

  /**
   * Find the first Element that matches the {@link Selector} CSS query, with this element as the starting context.
   * <p>This is effectively the same as calling {@code element.select(query).first()}, but is more efficient as query
   * execution stops on the first hit.</p>
   * <p>Also known as {@code querySelector()} in the Web DOM.</p>
   * @param cssQuery cssQuery a {@link Selector} CSS-like query
   * @return the first matching element, or <b>{@code null}</b> if there is no match.
   * @see #expectFirst(String)
   */
  selectFirst(cssQuery: string): Element {
    return Selector.selectFirst(cssQuery, this);
  }

  /**
   * Finds the first Element that matches the supplied Evaluator, with this element as the starting context, or
   * {@code null} if none match.
   *
   * @param evaluator an element evaluator
   * @return the first matching element (walking down the tree, starting from this element), or {@code null} if none
   * match.
   */
  selectFirstEvaluator(evaluator: Evaluator): Element | null {
    return Collector.findFirst(evaluator, this)
  }

  /**
   Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}. This
   is useful if you want to simply abort processing on a failed match.
   @param cssQuery a {@link Selector} CSS-like query
   @return the first matching element
   @throws IllegalArgumentException if no match is found
   @since 1.15.2
   */
  expectFirst(cssQuery: string): Element {
    let tip = this.parent() !== null ? `No elements matched the query ${cssQuery} on element ${this.getTagName()} .` :
      `No elements matched the query ${cssQuery} in the document.`
    return Validate.ensureNotNull(Selector.selectFirst(cssQuery, this), tip) as Element
  }

  /**
   * Checks if this element matches the given {@link Selector} CSS query. Also knows as {@code matches()} in the Web
   * DOM.
   *
   * @param cssQuery a {@link Selector} CSS query
   * @return if this element matches the query
   */
  public is(cssQuery: string|Evaluator): boolean {
    let evaluator:Evaluator
    if (typeof cssQuery === 'string') {
      evaluator = QueryParser.parse(cssQuery)
    } else {
      evaluator = cssQuery
    }
    return evaluator.matches(this.root(), this)
  }

  /**
   * Find the closest element up the tree of parents that matches the specified CSS query. Will return itself, an
   * ancestor, or {@code null} if there is no such matching element.
   * @param cssQuery a {@link Selector} CSS query
   * @return the closest ancestor element (possibly itself) that matches the provided evaluator. {@code null} if not
   * found.
   */
  closest(cssQuery: string): Element {
    return this.closestEvaluator(QueryParser.parse(cssQuery))
  }

  /**
   * Find the closest element up the tree of parents that matches the specified evaluator. Will return itself, an
   * ancestor, or {@code null} if there is no such matching element.
   * @param evaluator a query evaluator
   * @return the closest ancestor element (possibly itself) that matches the provided evaluator. {@code null} if not
   * found.
   */
  closestEvaluator(evaluator: Evaluator): Element | null {
    Validate.notNull(evaluator)
    let el = this as Element
    let root = this.root()
    if (evaluator.matches(root, el)) {
      return el
    }
    while (el !== undefined && el !== null) {
      if (evaluator.matches(root, el)) {
        return el
      }
      el = el.parent()
    }
    return null
  }

  /**
   Find Elements that match the supplied XPath expression.
   <p>Note that for convenience of writing the Xpath expression, namespaces are disabled, and queries can be
   expressed using the element's local name only.</p>
   <p>By default, XPath 1.0 expressions are supported. If you would to use XPath 2.0 or higher, you can provide an
   alternate XPathFactory implementation:</p>
   <ol>
   <li>Add the implementation to your classpath. E.g. to use <a href="https://www.saxonica.com/products/products.xml">Saxon-HE</a>, add <a href="https://mvnrepository.com/artifact/net.sf.saxon/Saxon-HE">net.sf.saxon:Saxon-HE</a> to your build.</li>
   <li>Set the system property <code>javax.xml.xpath.XPathFactory:jsoup</code> to the implementing classname. E.g.:<br>
   <code>System.setProperty(W3CDom.XPathFactoryProperty, "net.sf.saxon.xpath.XPathFactoryImpl");</code>
   </li>
   </ol>

   @param xpath XPath expression
   @return matching elements, or an empty list if none match.
   @see #selectXpath(String, Class)
   @since 1.14.3
   */
  selectXpath(xpath: string): Elements  {
    throw Error('not support')
    // return new Elements(NodeUtils.selectXpath(xpath, this, Element.class))
  }

  /**
   Find Nodes that match the supplied XPath expression.
   <p>For example, to select TextNodes under {@code p} elements: </p>
   <pre>List&lt;TextNode&gt; textNodes = doc.selectXpath("//body//p//text()", TextNode.class);</pre>
   <p>Note that in the jsoup DOM, Attribute objects are not Nodes. To directly select attribute values, do something
   like:</p>
   <pre>List&lt;String&gt; hrefs = doc.selectXpath("//a").eachAttr("href");</pre>
   @param xpath XPath expression
   @param nodeType the jsoup node type to return
   @see #selectXpath(String)
   @return a list of matching nodes
   @since 1.14.3
   */
  //   public <T extends Node> List<T> selectXpath(String xpath, Class<T> nodeType) {
  //   return NodeUtils.selectXpath(xpath, this, nodeType);
  // }

  /**
   * Insert a node to the end of this Element's children. The incoming node will be re-parented.
   *
   * @param child node to add.
   * @return this Element, for chaining
   * @see #prependChild(Node)
   * @see #insertChildren(int, Collection)
   */
  appendChild(child: Node): Element {
    Validate.notNull(child)
    // was - Node#addChildren(child). short-circuits an array create and a loop.
    this.reparentChild(child)
    this.ensureChildNodes()
    this._childNodes.add(child)
    child.setSiblingIndex(this._childNodes.length - 1);
    return this
  }

  /**
   Insert the given nodes to the end of this Element's children.

   @param children nodes to add
   @return this Element, for chaining
   @see #insertChildren(int, Collection)
   */
  appendChildren(children: List<Node>) {
    this.insertChildren(-1, children)
    return this
  }

  /**
   * Add this element to the supplied parent element, as its next child.
   *
   * @param parent element to which this element will be appended
   * @return this element, so that you can continue modifying the element
   */
  appendTo(parent: Element): Element {
    Validate.notNull(parent)
    parent.appendChild(this)
    return this
  }

  /**
   * Add a node to the start of this element's children.
   *
   * @param child node to add.
   * @return this element, so that you can add more child nodes or elements.
   */
  prependChild(child: Node): Element {
    Validate.notNull(child)
    this.addChildren1(0, [child])
    return this
  }

  /**
   Insert the given nodes to the start of this Element's children.

   @param children nodes to add
   @return this Element, for chaining
   @see #insertChildren(int, Collection)
   */
  prependChildren(children: List<Node>) {
    this.insertChildren(0, children)
    return this
  }

  /**
   * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the
   * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first.
   *
   * @param index 0-based index to insert children at. Specify {@code 0} to insert at the start, {@code -1} at the
   * end
   * @param children child nodes to insert
   * @return this element, for chaining.
   */
  insertChildren(index: number, children: List<Node>): Element {
    Validate.notNull(children, "Children collection to be inserted must not be null.")
    let currentSize = this.childNodeSize()
    if (index < 0) {
      index += currentSize + 1
    }
    Validate.isTrue(index >= 0 && index <= currentSize, "Insert position out of bounds.")
    let nodes: Node[] = []
    let i = 0
    for (let node of children) {
      nodes[i++] = node
    }
    this.addChildren1(index, nodes)
    return this
  }

  /**
   * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the
   * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first.
   *
   * @param index 0-based index to insert children at. Specify {@code 0} to insert at the start, {@code -1} at the
   * end
   * @param children child nodes to insert
   * @return this element, for chaining.
   */
  insertChildren1(index: number, children: Node[]): Element {
    Validate.notNull(children, "Children collection to be inserted must not be null.")
    let currentSize = this.childNodeSize()
    if (index < 0) {
      index += currentSize + 1
    }
    Validate.isTrue(index >= 0 && index <= currentSize, "Insert position out of bounds.")

    this.addChildren1(index, children)
    return this
  }

  /**
   * Create a new element by tag name, and add it as the last child.
   *
   * @param tagName the name of the tag (e.g. {@code div}).
   * @return the new element, to allow you to add content to it, e.g.:
   *  {@code parent.appendElement("h1").attr("id", "header").text("Welcome");}
   */
  public appendElement(tagName: string): Element {
    Validate.notEmpty(tagName,'Element appendElement tagName  must not be empty')
    let child = new Element(Tag.valueOf(tagName, NodeUtils.parser(this).getSettings()), this.baseUri())
    this.appendChild(child)
    return child
  }

  /**
   * Create a new element by tag name, and add it as the first child.
   *
   * @param tagName the name of the tag (e.g. {@code div}).
   * @return the new element, to allow you to add content to it, e.g.:
   *  {@code parent.prependElement("h1").attr("id", "header").text("Welcome");}
   */
  public prependElement(tagName: string): Element {
    Validate.notEmpty(tagName,'Element prependElement tagName  must not be empty')
    let child = new Element(Tag.valueOf(tagName, NodeUtils.parser(this).getSettings()), this.baseUri())
    this.prependChild(child);
    return child;
  }

  /**
   * Create and append a new TextNode to this element.
   *
   * @param text the (un-encoded) text to add
   * @return this element
   */
  public appendText(text: string): Element {
    Validate.notNull(text)
    let node = new TextNode(text)
    this.appendChild(node);
    return this;
  }

  /**
   * Create and prepend a new TextNode to this element.
   *
   * @param text the decoded text to add
   * @return this element
   */
  public prependText(text: string): Element {
    Validate.notNull(text)
    let node = new TextNode(text)
    this.prependChild(node)
    return this
  }

  /**
   * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children.
   * @param html HTML to add inside this element, after the existing HTML
   * @return this element
   * @see #html(String)
   */
  append(html: string): Element {
    Validate.notNull(html)
    let nodes = NodeUtils.parser(this).parseFragmentInput(html, this, this.baseUri())
    let cs: Node[] = []
    nodes.forEach((node, index) => {
      cs[index] = node
    })
    this.addChildren(cs)
    return this
  }

  /**
   * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children.
   * @param html HTML to add inside this element, before the existing HTML
   * @return this element
   * @see #html(String)
   */
  public prepend(html: string): Element {
    Validate.notNull(html)
    let nodes = NodeUtils.parser(this).parseFragmentInput(html, this, this.baseUri())
    let cs: Node[] = []
    nodes.forEach((node, index) => {
      cs[index] = node
    })
    this.addChildren1(0, cs)
    return this
  }

  /**
   * Insert the specified HTML into the DOM before this element (as a preceding sibling).
   *
   * @param html HTML to add before this element
   * @return this element, for chaining
   * @see #after(String)
   */
  public before(html: string | Node): Element {
    return super.before(html) as Element
  }

  /**
   * Insert the specified HTML into the DOM after this element (as a following sibling).
   *
   * @param html HTML to add after this element
   * @return this element, for chaining
   * @see #before(String)
   */
  public after(html: string): Element {
    return super.after(html) as Element
  }

  /**
   * Insert the specified node into the DOM after this node (as a following sibling).
   * @param node to add after this element
   * @return this element, for chaining
   * @see #before(Node)
   */
  public after1(node: Node): Element {
    return super.after1(node) as Element
  }

  /**
   * Remove all the element's child nodes. Any attributes are left as-is.
   * @return this element
   */
  public empty(): Element {
    this._childNodes.clear()
    return this
  }

  /**
   * Wrap the supplied HTML around this element.
   *
   * @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
   * @return this element, for chaining.
   */
  public wrap(html: string): Element {
    Validate.notEmpty(html,'Element wrap html must not be empty');

    let wrapChildren = NodeUtils.parser(this).parseFragmentInput(html, this.parent(), this.baseUri());
    let wrapNode = wrapChildren.get(0);
    // nothing to wrap with; noop
    if (!(wrapNode instanceof Element)) {
      return this;
    }


    let wrap = wrapNode as Element;
    let deepest = this.getDeepChild(wrap);
    if (this.parentNode !== undefined && this.parentNode !== null) {
      this.parentNode.replaceChild(this, wrap);
      // }
      deepest.addChildren([this]);

      // remainder (unbalanced wrap, like <div></div><p></p> -- The <p> is remainder
      if (wrapChildren.length > 0) {
        //noinspection ForLoopReplaceableByForEach (beacause it allocates an Iterator which is wasteful here)
        for (let i = 0; i < wrapChildren.length; i++) {
          let remainder = wrapChildren.get(i);
          // if no parent, this could be the wrap node, so skip
          if (wrap === remainder) {
            continue
          }
          if (remainder.parentNode !== undefined && remainder.parentNode !== null) {
            remainder.parentNode.removeChild(remainder);
          }
          wrap.after1(remainder);
        }
      }
      return this;
    }
  }

  private getDeepChild(el: Element): Element {
    let children = el.firstElementChild();
    while(children !== undefined && children !== null) {
      el = children
      children = children.firstElementChild()
    }
    return el;
  }

  /**
   * Get a CSS selector that will uniquely select this element.
   * <p>
   * If the element has an ID, returns #id;
   * otherwise returns the parent (if any) CSS selector, followed by {@literal '>'},
   * followed by a unique selector for the element (tag.class.class:nth-child(n)).
   * </p>
   *
   * @return the CSS Path that can be used to retrieve the element in a selector.
   */
  public cssSelector(): string {
    if (this.getId().length > 0) {
      // prefer to return the ID - but check that it's actually unique first!
      let idSel = `#${TokenQueue.escapeCssIdentifier(this.getId())}`
      let doc = this.ownerDocument()
      if (doc !== undefined && doc !== null) {
        let els = doc.select(idSel)
        if (els.length === 1 && els.get(0) === this) {
          return idSel
        }
      } else {
        // no ownerdoc, return the ID selector
        return idSel
      }
      // Escape tagname, and translate HTML namespace ns:tag to CSS namespace syntax ns|tag
      let tagName = TokenQueue.escapeCssIdentifier(this.getTagName()).replace("\\:", "|")
      let selector = StringUtil.borrowBuilder()
      selector.push(tagName)
      // String classes = StringUtil.join(classNames().stream().map(TokenQueue::escapeCssIdentifier).iterator(), ".");
      // todo - replace with ^^ in 1.16.1 when we enable Android support for stream etc
      let escapedClasses = new StringJoiner(".")
      this.classNames().forEach((name) => {
        escapedClasses.add(TokenQueue.escapeCssIdentifier(name))
      })
      let classes = escapedClasses.complete()
      if (classes.length > 0) {
        selector.push('.')
        selector.push(classes)
      }
      if (this.parent() === undefined || this.parent() === null || this.parent() instanceof Document) {
        return StringUtil.releaseBuilder(selector)
      }
      selector.splice(0, 0, " > ")
      if (this.parent().select(selector.toString()).length > 1) {
        selector.push(`:nth-child(${this.elementSiblingIndex() + 1})`)
      }
      return this.parent().cssSelector() + StringUtil.releaseBuilder(selector)
    }
  }

  /**
   * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling
   * of itself, so will not be included in the returned list.
   * @return sibling elements
   */
  public siblingElements(): Elements {
    if (this.parentNode === undefined || this.parentNode === null) {
      return new Elements()
    }
    let elements = this.parent().childElementsList()
    let siblings = new Elements()
    elements.forEach((element, index) => {
      if (element !== this) {
        siblings.add(element)
      }
    })
    return siblings
  }

  /**
   * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s,
   * the {@code nextElementSibling} of the first {@code p} is the second {@code p}.
   * <p>
   * This is similar to {@link #nextSibling()}, but specifically finds only Elements
   * </p>
   * @return the next element, or null if there is no next element
   * @see #previousElementSibling()
   */
  public nextElementSibling(): Element | null {
    let next = this as Node
    while ((next = next.nextSibling()) !== null) {
      if (next instanceof Element) {
        return next as Element
      }
    }
    return null
  }

  /**
   * Get each of the sibling elements that come after this element.
   *
   * @return each of the element siblings after this element, or an empty list if there are no next sibling elements
   */
  public nextElementSiblings(): Elements {
    return this.nextElementSiblings1(true)
  }

  /**
   * Gets the previous element sibling of this element.
   * @return the previous element, or null if there is no previous element
   * @see #nextElementSibling()
   */
  public previousElementSibling(): Element | null {
    let prev = this as Node
    while ((prev = prev.previousSibling()) !== null) {
      if (prev instanceof Element) {
        return prev as Element
      }
    }
    return null
  }

  /**
   * Get each of the element siblings before this element.
   *
   * @return the previous element siblings, or an empty list if there are none.
   */
  public previousElementSiblings(): Elements {
    return this.nextElementSiblings1(false)
  }

  private nextElementSiblings1(next: boolean): Elements {
    let els = new Elements()
    if (this.parentNode === undefined || this.parentNode === null) {
      return els
    }
    els.add(this)
    return next ? els.nextAll() : els.prevAll()
  }

  /**
   * Gets the first Element sibling of this element. That may be this element.
   * @return the first sibling that is an element (aka the parent's first element child)
   */
  public firstElementSibling(): Element {
    if (this.parent() !== undefined && this.parent() !== null) {
      //noinspection DataFlowIssue (not nullable, would be this is no other sibs)
      return this.parent().firstElementChild();
    } else {
      return this
    }
  }

  /**
   * Get the list index of this element in its element sibling list. I.e. if this is the first element
   * sibling, returns 0.
   * @return position in element sibling list
   */
  public elementSiblingIndex(): number {
    if (this.parent() === undefined || this.parent() === null) {
      return 0
    }
    return Element.indexInList(this, this.parent().childElementsList())
  }

  /**
   * Gets the last element sibling of this element. That may be this element.
   * @return the last sibling that is an element (aka the parent's last element child)
   */
  public lastElementSibling(): Element {
    if (this.parent() !== undefined && this.parent() !== null) {
      //noinspection DataFlowIssue (not nullable, would be this is no other sibs)
      return this.parent().lastElementChild();
    } else {
      return this
    }
  }

  private static indexInList(search: Element, elements: List<Element>): number {
    let size = elements.length
    for (let index = 0; index < size; index++) {
      if (elements[index] === search) {
        return index
      }
    }
    return 0
  }

  /**
   Gets the first child of this Element that is an Element, or {@code null} if there is none.
   @return the first Element child node, or null.
   @see #firstChild()
   @see #lastElementChild()
   @since 1.15.2
   */
  public firstElementChild(): Element | null {
    let child = this.firstChild()
    while (child !== undefined && child !== null) {
      if (child instanceof Element) {
        return child as Element
      }
      child = child.nextSibling();
    }
    return null
  }

  /**
   Gets the last child of this Element that is an Element, or @{code null} if there is none.
   @return the last Element child node, or null.
   @see #lastChild()
   @see #firstElementChild()
   @since 1.15.2
   */
  public lastElementChild(): Element | null {
    let child = this.lastChild()
    while (child !== undefined && child !== null) {
      if (child instanceof Element) {
        return child as Element
      }
      child = child.previousSibling();
    }
    return null
  }

  // DOM type methods

  /**
   * Finds elements, including and recursively under this element, with the specified tag name.
   * @param tagName The tag name to search for (case insensitively).
   * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match.
   */
  public getElementsByTag(tagName: string): Elements {
    Validate.notEmpty(tagName,'Element getElementsByTag tagName must not be empty')
    tagName = Normalizer.normalize(tagName)
    return Collector.collect(new EvaluatorTag(tagName), this)
  }

  /**
   * Find an element by ID, including or under this element.
   * <p>
   * Note that this finds the first matching ID, starting with this element. If you search down from a different
   * starting point, it is possible to find a different element by ID. For unique element by ID within a Document,
   * use {@link Document#getElementById(String)}
   * @param id The ID to search for.
   * @return The first matching element by ID, starting with this element, or null if none found.
   */
  public getElementById(id: string): Element | null {
    Validate.notEmpty(id,'Element getElementById id must not be empty')
    let elements = Collector.collect(new EvaluatorId(id), this)
    if (elements.length > 0) {
      return elements[0]
    } else {
      return null
    }
  }

  /**
   * Find elements that have this class, including or under this element. Case-insensitive.
   * <p>
   * Elements can have multiple classes (e.g. {@code <div class="header round first">}). This method
   * checks each class, so you can find the above with {@code el.getElementsByClass("header");}.
   *
   * @param className the name of the class to search for.
   * @return elements with the supplied class name, empty if none
   * @see #hasClass(String)
   * @see #classNames()
   */
  public getElementsByClass(className: string): Elements {
    Validate.notEmpty(className,'Element getElementsByClass className must not be empty')
    return Collector.collect(new EvaluatorClass(className), this)
  }

  /**
   * Find elements that have a named attribute set. Case-insensitive.
   *
   * @param key name of the attribute, e.g. {@code href}
   * @return elements that have this attribute, empty if none
   */
  public getElementsByAttribute(key: string): Elements {
    Validate.notEmpty(key,'Element getElementsByAttribute key must not be empty')
    key = key.trim()
    return Collector.collect(new EvaluatorAttribute(key), this)
  }

  /**
   * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements
   * that have HTML5 datasets.
   * @param keyPrefix name prefix of the attribute e.g. {@code data-}
   * @return elements that have attribute names that start with the prefix, empty if none.
   */
  public getElementsByAttributeStarting(keyPrefix: string): Elements {
    Validate.notEmpty(keyPrefix,'Element getElementsByAttributeStarting keyPrefix must not be empty')
    keyPrefix = keyPrefix.trim()
    return Collector.collect(new EvaluatorAttributeStarting(keyPrefix), this)
  }

  /**
   * Find elements that have an attribute with the specific value. Case-insensitive.
   *
   * @param key name of the attribute
   * @param value value of the attribute
   * @return elements that have this attribute with this value, empty if none
   */
  public getElementsByAttributeValue(key: string, value: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValue(key, value), this)
  }

  /**
   * Find elements that either do not have this attribute, or have it with a different value. Case-insensitive.
   *
   * @param key name of the attribute
   * @param value value of the attribute
   * @return elements that do not have a matching attribute
   */
  public getElementsByAttributeValueNot(key: string, value: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValueNot(key, value), this);
  }

  /**
   * Find elements that have attributes that start with the value prefix. Case-insensitive.
   *
   * @param key name of the attribute
   * @param valuePrefix start of attribute value
   * @return elements that have attributes that start with the value prefix
   */
  public getElementsByAttributeValueStarting(key: string, valuePrefix: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValueStarting(key, valuePrefix), this)
  }

  /**
   * Find elements that have attributes that end with the value suffix. Case-insensitive.
   *
   * @param key name of the attribute
   * @param valueSuffix end of the attribute value
   * @return elements that have attributes that end with the value suffix
   */
  public getElementsByAttributeValueEnding(key: string, valuePrefix: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValueEnding(key, valuePrefix), this);
  }

  /**
   * Find elements that have attributes whose value contains the match string. Case-insensitive.
   *
   * @param key name of the attribute
   * @param match substring of value to search for
   * @return elements that have attributes containing this text
   */
  public getElementsByAttributeValueContaining(key: string, match: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValueContaining(key, match), this);
  }

  /**
   * Find elements that have an attribute whose value matches the supplied regular expression.
   * @param key name of the attribute
   * @param pattern compiled regular expression to match against attribute values
   * @return elements that have attributes matching this regular expression
   */
  public getElementsByAttributeValueMatching(key: string, pattern: string): Elements {
    return Collector.collect(new EvaluatorAttributeWithValueMatching(key, pattern), this);
  }

  /**
   * Find elements whose sibling index is less than the supplied index.
   * @param index 0-based index
   * @return elements less than index
   */
  public getElementsByIndexLessThan(index: number): Elements {
    return Collector.collect(new EvaluatorIndexLessThan(index), this);
  }

  /**
   * Find elements whose sibling index is greater than the supplied index.
   * @param index 0-based index
   * @return elements greater than index
   */
  public getElementsByIndexGreaterThan(index: number): Elements {
    return Collector.collect(new EvaluatorIndexGreaterThan(index), this);
  }

  /**
   * Find elements whose sibling index is equal to the supplied index.
   * @param index 0-based index
   * @return elements equal to index
   */
  public getElementsByIndexEquals(index: number): Elements {
    return Collector.collect(new EvaluatorIndexEquals(index), this);
  }

  /**
   * Find elements that contain the specified string. The search is case-insensitive. The text may appear directly
   * in the element, or in any of its descendants.
   * @param searchText to look for in the element's text
   * @return elements that contain the string, case-insensitive.
   * @see Element#text()
   */
  public getElementsContainingText(searchText: string): Elements {
    return Collector.collect(new EvaluatorContainsText(searchText), this);
  }

  /**
   * Find elements that directly contain the specified string. The search is case-insensitive. The text must appear directly
   * in the element, not in any of its descendants.
   * @param searchText to look for in the element's own text
   * @return elements that contain the string, case-insensitive.
   * @see Element#ownText()
   */
  public getElementsContainingOwnText(searchText: string): Elements {
    return Collector.collect(new EvaluatorContainsOwnText(searchText), this);
  }

  /**
   * Find elements whose text matches the supplied regular expression.
   * @param pattern regular expression to match text against
   * @return elements matching the supplied regular expression.
   * @see Element#text()
   */
  public getElementsMatchingText(regex: string): Elements {
    return Collector.collect(new EvaluatorMatches(regex), this);
  }


  /**
   * Find elements whose own text matches the supplied regular expression.
   * @param pattern regular expression to match text against
   * @return elements matching the supplied regular expression.
   * @see Element#ownText()
   */
  public getElementsMatchingOwnText(regex: string): Elements {
    return Collector.collect(new EvaluatorMatchesOwn(regex), this);
  }

  /**
   * Find all elements under this element (including self, and children of children).
   *
   * @return all elements
   */
  public getAllElements(): Elements {
    return Collector.collect(new EvaluatorAllElements(), this);
  }

  /**
   Gets the <b>normalized, combined text</b> of this element and all its children. Whitespace is normalized and
   trimmed.
   <p>For example, given HTML {@code <p>Hello  <b>there</b> now! </p>}, {@code p.text()} returns {@code "Hello there
   now!"}
   <p>If you do not want normalized text, use {@link #wholeText()}. If you want just the text of this node (and not
   children), use {@link #ownText()}
   <p>Note that this method returns the textual content that would be presented to a reader. The contents of data
   nodes (such as {@code <script>} tags) are not considered text. Use {@link #data()} or {@link #html()} to retrieve
   that content.

   @return decoded, normalized text, or empty string if none.
   @see #wholeText()
   @see #ownText()
   @see #textNodes()
   */
  public text(): string {
    let accum = StringUtil.borrowBuilder()
    this.traverse({
      head(node: Node, depth: number) {
        if (node instanceof TextNode) {
          let textNode = node as TextNode
          Element.appendNormalisedText(accum, textNode)
        } else if (node instanceof Element) {
          let element = node as Element
          if (accum.length > 0 && (element.isBlock() || element.isNode("br")) &&
          !TextNode.lastCharIsWhitespace(accum)) {
            accum.push(' ')
          }
        }
      },
      tail(node: Node, depth: number) {
        // make sure there is a space between block tags and immediately following text nodes or inline elements <div>One</div>Two should be "One Two".
        if (node instanceof Element) {
          let element = node as Element
          let next = node.nextSibling()
          if (element.isBlock() && (next instanceof TextNode || (next instanceof Element && next._tag.formatAsBlock()))
          && !TextNode.lastCharIsWhitespace(accum)) {
            accum.push(' ')
          }
        }
      }
    },this)
    return StringUtil.releaseBuilder(accum).trim()
  }

  private static appendWholeText(node: Node, accum: string[]) {
    if (node instanceof TextNode) {
      accum.push((node as TextNode).getWholeText());
    } else if (node.isNode("br")) {
      accum.push("\n");
    }
  }

  /**
   Get the non-normalized, decoded text of this element and its children, including only any newlines and spaces
   present in the original source.
   @return decoded, non-normalized text
   @see #text()
   @see #wholeOwnText()
   */
  public wholeText(): string {
    let accum = StringUtil.borrowBuilder()
    this.traverse({
      head(node: Node, depth: number) {
        Element.appendWholeText(node, accum)
      },
      tail(node: Node, depth: number) {
      }
    },this)
    return StringUtil.releaseBuilder(accum)
  }

  /**
   Get the non-normalized, decoded text of this element, <b>not including</b> any child elements, including only any
   newlines and spaces present in the original source.
   @return decoded, non-normalized text that is a direct child of this Element
   @see #text()
   @see #wholeText()
   @see #ownText()
   @since 1.15.1
   */
  public wholeOwnText(): string {
    let accum = StringUtil.borrowBuilder()
    let size = this.childNodeSize()
    for (let i = 0; i < size; i++) {
      let node = this._childNodes.get(i)
      Element.appendWholeText(node, accum)
    }
    return StringUtil.releaseBuilder(accum)
  }

  /**
   * Gets the (normalized) text owned by this element only; does not get the combined text of all children.
   * <p>
   * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.ownText()} returns {@code "Hello now!"},
   * whereas {@code p.text()} returns {@code "Hello there now!"}.
   * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element.
   *
   * @return decoded text, or empty string if none.
   * @see #text()
   * @see #textNodes()
   */
  public ownText(): string {
    let accum = StringUtil.borrowBuilder()
    let size = this.childNodeSize()
    for (let i = 0; i < size; i++) {
      let child = this._childNodes.get(i);
      if (child instanceof TextNode) {
        let textNode = child as TextNode;
        Element.appendNormalisedText(accum, textNode);
      } else if (child.isNode("br") /*&& !TextNode.lastCharIsWhitespace(accum)*/) {
        accum.push(" ");
      }
    }
    return StringUtil.releaseBuilder(accum)
  }

  private static appendNormalisedText(accum: string[], textNode: TextNode) {
    let text = textNode.getWholeText()
    if (Element.preserveWhitespace(textNode.parentNode) || textNode instanceof CDataNode) {
      accum.push(text)
    } else {
      StringUtil.appendNormalisedWhitespace(accum, text, TextNode.lastCharIsWhitespace(accum))
    }
  }

  public static preserveWhitespace(node: Node): boolean {
    // looks only at this element and five levels up, to prevent recursion & needless stack searches
    if (node instanceof Element) {
      let el = node as Element
      if (el._tag.preserveWhitespace()) {
        return true
      }
      el = el.parent()
      let i = 1
      while (i < 6 && el !== undefined && el !== null) {
        if (el._tag.preserveWhitespace()) {
          return true
        }
        el = el.parent()
        i++
      }
    }
    return false
  }

  /**
   * Set the text of this element. Any existing contents (text or elements) will be cleared.
   * <p>As a special case, for {@code <script>} and {@code <style>} tags, the input text will be treated as data,
   * not visible text.</p>
   * @param text decoded text
   * @return this element
   */
  public setText(text: string): Element {
    Validate.notNull(text)
    this.empty()
    // special case for script/style in HTML: should be data node
    let owner = this.ownerDocument();
    // an alternate impl would be to run through the parser
    if (owner !== undefined && owner !== null && owner.getParser().isContentForTagData(this.normalName())) {
      this.appendChild(new DataNode(text))
    } else {
      this.appendChild(new TextNode(text))
    }
    return this
  }

  /**
   Checks if the current element or any of its child elements contain non-whitespace text.
   @return {@code true} if the element has non-blank text content, {@code false} otherwise.
   */
  public hasText(): boolean {
    let buffer = new SharedArrayBuffer(4)
    let view = new Int32Array(buffer);
    Atomics.compareExchange(view, 0, 1, 0)
    this.filter({
      head(node: Node, depth: number): FilterResult {
        if (node instanceof TextNode) {
          let textNode = node as TextNode
          if (!textNode.isBlank()) {
            Atomics.compareExchange(view, 0, 0, 1)
            return FilterResult.STOP
          }
        }
        return FilterResult.CONTINUE
      },
      tail(node: Node, depth: number): FilterResult {
        return FilterResult.CONTINUE
      }
    })
    return Atomics.load(view, 0) === 1
  }

  /**
   * Get the combined data of this element. Data is e.g. the inside of a {@code <script>} tag. Note that data is NOT the
   * text of the element. Use {@link #text()} to get the text that would be visible to a user, and {@code data()}
   * for the contents of scripts, comments, CSS styles, etc.
   *
   * @return the data, or empty string if none
   *
   * @see #dataNodes()
   */
  public data(): string {
    let sb = StringUtil.borrowBuilder()
    this.traverse({
      head(node: Node, depth: number) {
        if (node instanceof DataNode) {
          let data = node as DataNode
          sb.push(data.getWholeData())
        } else if (node instanceof Comment) {
          let comment = node as Comment
          sb.push(comment.getData())
        } else if (node instanceof CDataNode) {
          // this shouldn't really happen because the html parser won't see the cdata as anything special when parsing script.
          // but in case another type gets through.
          let cDataNode = node as CDataNode
          sb.push(cDataNode.getWholeText())
        }
      },
      tail(node: Node, depth: number) {
      }
    },this)
    return StringUtil.releaseBuilder(sb);
  }

  /**
   * Gets the literal value of this element's "class" attribute, which may include multiple class names, space
   * separated. (E.g. on <code>&lt;div class="header gray"&gt;</code> returns, "<code>header gray</code>")
   * @return The literal class attribute, or <b>empty string</b> if no class attribute set.
   */
  public className(): string {
    return this.getAttr("class").trim()
  }

  /**
   * Get each of the element's class names. E.g. on element {@code <div class="header gray">},
   * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to
   * the backing {@code class} attribute; use the {@link #classNames(java.util.Set)} method to persist them.
   * @return set of classnames, empty if no class attribute
   */
  public classNames(): Set<string> {
    let names = this.className().split(Element._ClassSplit)
    let classNames = new Set<string>()
    names.forEach((name, index) => {
      classNames.add(name)
    })
    classNames.delete(""); // if classNames() was empty, would include an empty class
    return classNames
  }

  /**
   Set the element's {@code class} attribute to the supplied class names.
   @param classNames set of classes
   @return this element, for chaining
   */
  public setClassNames(classNames: Set<string>): Element {
    Validate.notNull(classNames)
    if (classNames.size === 0) {
      this.attributes().remove('class')
    } else {
      this.attributes().put('class', StringUtil.join(classNames, " "))
    }
    return this
  }

  /**
   * Tests if this element has a class. Case-insensitive.
   * @param className name of class to check for
   * @return true if it does, false if not
   */
  public hasClass(className: string): boolean {
    if (this._attributes === undefined || this._attributes === null) {
      return false
    }
    let classAttr = this._attributes.getIgnoreCase("class")
    let len = classAttr.length
    let wantLen = className.length
    if (len === 0 || len < wantLen) {
      return false
    }
    // if both lengths are equal, only need compare the className with the attribute
    if (len === wantLen) {
      return className.toLowerCase() === classAttr.toLowerCase()
    }
    // otherwise, scan for whitespace and compare regions (with no string or arraylist allocations)
    let inClass = false
    let start = 0
    for (let i = 0; i < len; i++) {
      if (Character.isWhitespace(classAttr.charAt(i))) {
        if (inClass) {
          // white space ends a class name, compare it with the requested one, ignore case
          if (i - start === wantLen && Character.regionMatches(true, classAttr, start, className, 0, wantLen)) {
            return true;
          }
          inClass = false;
        }
      } else {
        if (!inClass) {
          // we're in a class name : keep the start of the substring
          inClass = true;
          start = i;
        }
      }
    }
    // check the last entry
    if (inClass && len - start == wantLen) {
      return Character.regionMatches(true, classAttr, start, className, 0, wantLen);
    }

    return false;
  }

  /**
   Add a class name to this element's {@code class} attribute.
   @param className class name to add
   @return this element
   */
  public addClass(className: string): Element {
    Validate.notNull(className)
    let classes = this.classNames()
    classes.add(className)
    this.setClassNames(classes)
    return this
  }

  /**
   Remove a class name from this element's {@code class} attribute.
   @param className class name to remove
   @return this element
   */
  public removeClass(className: string): Element {
    Validate.notNull(className)
    let classes = this.classNames()
    classes.delete(className)
    this.setClassNames(classes)
    return this
  }

  /**
   Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it.
   @param className class name to toggle
   @return this element
   */
  public toggleClass(className: string): Element {
    Validate.notNull(className)
    let classes = this.classNames()
    if (classes.has(className)) {
      classes.delete(className)
    } else {
      classes.add(className)
    }
    this.setClassNames(classes)
    return this
  }

  /**
   * Get the value of a form element (input, textarea, etc).
   * @return the value of the form element, or empty string if not set.
   */
  public getVal(): string {
    if (this.normalName() === "textarea") {
      return this.text()
    } else {
      return this.getAttr("value")
    }
  }

  /**
   * Set the value of a form element (input, textarea, etc).
   * @param value value to set
   * @return this element (for chaining)
   */
  public setVal(value: string): Element {
    if (this.normalName() === "textarea") {
      this.setText(value)
    } else {
      this.setAttr("value", value)
    }
    return this
  }

  /**
   Get the source range (start and end positions) of the end (closing) tag for this Element. Position tracking must be
   enabled prior to parsing the content.
   @return the range of the closing tag for this element, if it was explicitly closed in the source. {@code Untracked}
   otherwise.
   @see org.jsoup.parser.Parser#setTrackPosition(boolean)
   @see Node#sourceRange()
   @since 1.15.2
   */
  public endSourceRange(): Range {
    return Range.of(this, false)
  }

  shouldIndent(out: OutputSettings): boolean {
    return out.getPrettyPrint() && this.isFormatAsBlock(out) && !this.isInlineable(out) && !Element.preserveWhitespace(this.parentNode)
  }

  outerHtmlHead(accum: string[], depth: number, out: OutputSettings) {
    if (this.shouldIndent(out)) {
      if (accum.length > 0) {
        this.indent(accum, depth, out)
      }
    }
    accum.push('<')
    accum.push(this.getTagName())
    if (this._attributes !== undefined && this._attributes !== null) {
      this._attributes.html1(accum, out)
    }
    // selfclosing includes unknown tags, isEmpty defines tags that are always empty
    if (this._childNodes.length === 0 && this._tag.isSelfClosing()) {
      if (out.getSyntax() === Syntax.html && this._tag.isEmpty()) {
        accum.push('>')
      } else {
        // <img> in html, <img /> in xml
        accum.push(' />')
      }
    } else {
      accum.push('>')
    }
  }

  outerHtmlTail(accum: string[], depth: number, out: OutputSettings) {
    if (!(this._childNodes.isEmpty() && this._tag.isSelfClosing())) {
      if (out.getPrettyPrint() && (!this._childNodes.isEmpty() && (
        (this._tag.formatAsBlock() && !Element.preserveWhitespace(this.parentNode)) ||
        (out.getOutline() && (this._childNodes.length > 1 || (this._childNodes.length === 1 && this._childNodes.get(0) instanceof Element)))))) {
        this.indent(accum, depth, out)
      }
      accum.push('</')
      accum.push(this.getTagName())
      accum.push('>')
    }
  }

  /**
   * Retrieves the element's inner HTML. E.g. on a {@code <div>} with one empty {@code <p>}, would return
   * {@code <p></p>}. (Whereas {@link #outerHtml()} would return {@code <div><p></p></div>}.)
   *
   * @return String of HTML.
   * @see #outerHtml()
   */
  public getHtml(): string {
    let accum = StringUtil.borrowBuilder()
    this.html(accum)
    let html = StringUtil.releaseBuilder(accum)
    return NodeUtils.outputSettings(this).getPrettyPrint() ? html.trim() : html;
  }

  public html(appendable: string[]): string[] {
    let size = this._childNodes.length
    for (let i = 0; i < size; i++) {
      this._childNodes.get(i).outerHtml1(appendable)
    }
    return appendable;
  }

  /**
   * Set this element's inner HTML. Clears the existing HTML first.
   * @param html HTML to parse and set into this element
   * @return this element
   * @see #append(String)
   */
  public setHtml(html: string): Element {
    this.empty()
    this.append(html)
    return this
  }

  public clone(): Element {
    return super.clone() as Element
  }

  public shallowClone(): Element {
    // simpler than implementing a clone version with no child copy
    return new Element(this._tag, this.baseUri(), (this._attributes === undefined || this._attributes === null) ? null : this._attributes.clone())
  }

  public doClone(parent: Node): Element {
    let clone = super.doClone(parent) as Element
    clone._attributes = (this._attributes !== undefined && this._attributes !== null) ? this._attributes.clone() : null
    clone._childNodes = new List<Node>()
    this._childNodes.forEach((node, index) => {
      clone._childNodes.add(node.clone())
    })
    return clone;
  }

  public clearAttributes(): Element {
    if (this._attributes !== undefined && this._attributes !== null) {
      super.clearAttributes()
      this._attributes = null
    }
    return this
  }

  public removeAttr(attributeKey: string): Element {
    return super.removeAttr(attributeKey) as Element
  }

  public root(): Element {
    // probably a document, but always at least an element
    return super.root() as Element
  }


  public forEachNode(action: (node: Node) => void): Element {
    return super.forEachNode(action) as Element;
  }

  /**
   Perform the supplied action on this Element and each of its descendant Elements, during a depth-first traversal.
   Elements may be inspected, changed, added, replaced, or removed.
   @param action the function to perform on the element
   @return this Element, for chaining
   @see Node#forEachNode(Consumer)
   */
  public forEach(action: (node: Element) => void): Element {
    Validate.notNull(action)
    this.traverse({ head(node: Node, depth: number) {
      if (node instanceof Element) {
        action(node)
      }
    }, tail(node: Node, depth: number) {
    } }, this)
    return this
  }

  public filter(nodeFilter: NodeFilter): Element {
    return super.filter(nodeFilter) as Element
  }

  private isFormatAsBlock(out: OutputSettings): boolean {
    return this._tag.isBlock() ||
    (this.parent() !== undefined && this.parent() !== null && this.parent().getTag().formatAsBlock()) || out.getOutline()
  }

  private isInlineable(out: OutputSettings): boolean {
    if (!this._tag.isInline()) {
      return false
    }
    return (this.parent() === undefined || this.parent() === null || this.parent().isBlock())
    && !this.isEffectivelyFirst()
    && !out.getOutline()
    && !this.isNode("br")
  }

  protected  addSiblingHtml(index: number, html: string) {
    Validate.notNull(html);
    Validate.notNull(this.parentNode);
    let nodes = NodeUtils.parser(this).parseFragmentInput(html, this.parent(), this.baseUri());
    let ans: Node[] = []
    for (let index = 0; index < nodes.length; index++) {
      ans[index] = nodes[index];
    }
    this.parentNode.addChildren1(index, ans);
  }


}


